• 一个对象是否是线程安全的,取决于它是否被多个线程访问,要使得对象是线程安全的,需要采用同步机制来协同对可变状态的访问

  • 在线程安全类中封装了必要的同步机制,因此客户端无需进一步采用同步措施

  • 无状态对象一定是线程安全的,大多数Servlet都是无状态的,从而极大的降低了在实现Servlet线程安全性时的复杂性

  • 为了确保线程安全性,先检查后执行和读取-修改-写入等操作必须是原子性的

    • 先检查后执行(延迟初始化)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class LazyInitRace {
      private ExpensiveObject instance = null;
      public ExpensiveObject getInstance() {
      if (instance == null) {
      instance = new ExpensiveObject();
      }
      return instance;
      }
      }

      假设线程A和线程B同时执行getInstance()方法,A看到instance为空,因而创建一个新的ExpensiveObject实例,B同样需要判断instance是否为空,此时的instance是否为空,要取决于不可预测的时序,包括线程的调度方式,以及A需要花多长时间来初始化ExpensiveObject并设置instance,如果当B检查时,instance为空,那么在两次调用getInstance()时可能会得到不同的结果

    • 读取-修改-写入

  • 当在无状态的类中添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class CountingFactorizer implements Servlet {
    private final AtomicLong count = new AtomicLong(0);
    public long getCount() {
    return count.get();
    }
    public void service(ServletRequest request, ServletResponse response) {
    BigInteger i = extarctFromRequest(request);
    BigInteger[] factors = factor(i);
    count.incrementAndGet();
    encodeIntoResponse(response, factors);
    }
    }
  • 内置锁是可重入的,如果某个线程试图获得一个由它自己持有的锁,那么这个请求就会成功。在下面的代码中,子类改写了父类的synchronized方法,然后调用父类中的方法,如果此时没有可重入的锁,那么这段代码就会产生死锁,由于Widget和LoggingWidget中doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获取Widget上的锁,然而如果内置锁是不可重入的,那么在调用super.doSomething()时将无法获得Widget上的锁,因为这个锁已经被持有,从而线程将永远停顿下去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Widget {
    public synchronized void doSomething() {
    }
    }
    public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
    super.doSomething();
    }
    }
  • 如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置都需要使用同步,而且,当使用锁来协调对某个变量的访问时,在访问变量的所有位置上都需要使用同一个锁。一种常见的错误是只有在写入共享变量时才需要使用同步,然而事实并非如此

    对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。

  • 对象的域并不一定要通过内置锁来保护,当获取与对象关联的锁时,并不能阻止另外一个线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁(其他的线程仍然可以访问该对象中未使用对象锁保护的代码)。

  • 并非所有的数据都需要使用锁进行保护,只有被多个线程同时访问的可变数据才需要使用锁来保护。

  • 尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去。

  • 看下面这段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class CacheFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BitInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    public synchronized long getHits() {
    return hits;
    }
    public synchronized double getCacheHitRatio() {
    return (double) cacheHits / (double) hits;
    }
    public void service(ServletRequest request, ServletResponse response) {
    BigInteger i = extarctFromRequest(request);
    BigInteger[] factors = null;
    synchronized(this) {
    ++hits;
    if (i.equals(lastNumber)) {
    ++cacheHits;
    factors = lastFactors.clone();
    }
    }
    if (factors == null) {
    factors = factor(i); // 将耗时操作从同步代码中分离出来
    synchronized(this) {
    lastNumber = i;
    lastFactors = factors.clone();
    }
    }
    encodeIntoResponse(response, factors);
    }
    }

    重新构造后的CacheFactorizer实现了在简单性与并发性之间的平衡。在获取和释放锁等操作上都需要一定的开销,因此如果将同步代码块分解的过细,那么通常并不好,尽管这样做并不会破坏原子性。当访问状态变量或者在复合操作的执行期间,CacheFactorizer需要持有锁,但在执行时间较长的因数分解运算之前需要释放锁,这样既确保了线程安全性,也不会过多的影响并发性,而且在每个同步代码块中的代码路径都足够短。

  • 当执行时间较长的或者可能无法快速完成的操作时(网络IO或控制台IO),一定不要持有锁